import fs from 'fs'
import { extname, isAbsolute, join, normalize, parse, resolve, sep } from 'path'
import createError from '../../../http/httpErrors'
const fsPromises = fs.promises
const { stat } = fsPromises

export default async function send(ctx, pathname, opts = {}) {
  // 先解析配置项
  const { root, index, cacheControl, format } = opts

  const trailingSlash = pathname[pathname.length - 1] === '/'    // 看看path结尾是不是/
  pathname = pathname.substring(parse(pathname).root.length)         // 去掉path开头的/

  pathname = decode(pathname)     // 其实就是decodeURIComponent， decode辅助方法在后面
  if(pathname === -1) return ctx.throw(400, 'failed to decode')

  // 如果请求以/结尾，肯定是一个文件夹，将path改为文件夹下面的默认文件
  if(index && trailingSlash) pathname += index

  // 将一个根路径和请求的相对路径合并成一个绝对路径
  // 并且防止一些常见的攻击，比如GET /../file.js
  pathname = resolvePath(root, pathname)

  // 用fs.stat获取文件的基本信息，顺便检测下文件存在不
  let stats
  try {
    stats = await stat(pathname)

    // 如果是文件夹，并且format为true，拼上index文件
    if(stats.isDirectory()) {
      if(format && index) {
        pathname += `/${index}`
        stats = await stat(pathname)
      } else {
        return
      }
    }
  } catch(err) {
    // 错误处理，如果是文件不存在，返回404，否则返回500
    const notfound = ['ENOENT', 'ENAMETOOLONG', 'ENOTDIR']
    if(notfound.includes(err.code)) {
      // 快速创建HTTP错误对象
      throw createError(404, err)
    }
    err.status = 500
    throw err
  }

  // 设置Content-Length的header
  ctx.set('Content-Length', stats.size)
  // 设置缓存控制header
  if(!ctx.response.get('Last-Modified')) ctx.set('Last-Modified', stats.mtime.toUTCString())
  if(!ctx.response.get('Cache-Control'))  ctx.set('Cache-Control', cacheControl)

  // 设置返回类型和返回内容
  if(!ctx.type) ctx.type = extname(pathname)
  ctx.body = fs.createReadStream(pathname)

  return pathname
}

function decode(path) {
  try {
    return decodeURIComponent(path)
  } catch(err) {
    return -1
  }
}

const UP_PATH_REGEXP = /(?:^|[\\/])\.\.(?:[\\/]|$)/

/**
 * Resolve relative path against a root path
 *
 * @param {string} rootPath
 * @param {string} relativePath
 * @return {string}
 */
function resolvePath(rootPath, relativePath) {
  let path = relativePath
  let root = rootPath

  // root is optional, similar to root.resolve
  if(arguments.length === 1) {
    path = rootPath
    root = process.cwd()
  }

  if(root == null) {
    throw new TypeError('argument rootPath is required')
  }

  if(typeof root !== 'string') {
    throw new TypeError('argument rootPath must be a string')
  }

  if(path == null) {
    throw new TypeError('argument relativePath is required')
  }

  if(typeof path !== 'string') {
    throw new TypeError('argument relativePath must be a string')
  }

  // containing NULL bytes is malicious
  if(path.indexOf('\0') !== -1) {
    throw createError(400, 'Malicious Path')
  }

  // path should never be absolute
  if(isAbsolute(path)) {
    throw createError(400, 'Malicious Path')
  }

  // path outside root
  if(UP_PATH_REGEXP.test(normalize('.' + sep + path))) {
    throw createError(403)
  }

  // join the relative path
  return normalize(join(resolve(root), path))
}
